在 Day23,我們提到了像 messageDetails
這樣只涉及少數元件的狀態,使用局部管理可以避免不必要的全域依賴並保持程式碼的簡潔。
但聰明的你應該已經發現了,若新增功能的成功與失敗有提示訊息,那麼編輯成功與失敗也應該要有提示訊息,以保持 UI 的一致性。
在最初設計時,沒有顧慮到這一點,若要依然使用局部狀態管理來處理提示訊息,狀態會經過 App => TodoList => Todo
這樣的路徑傳遞。這不但會讓程式碼變得複雜,還可能導致重複的邏輯。因此,決定使用 useReducer
與 useContext
來重構提示訊息管理,讓這部分邏輯統一放在全域狀態中處理。
若你對於 useReducer
與 useContext
已經非常熟悉,可以直接跳過此篇,因為我們的 Todo List 基本功能已經完成,後續的幾篇不會再與 Todo List 有關,若你對於 useReducer
與 useContext
並不是那麼的熟悉,歡迎跟著我一起往下實作。
首先,我們要修改 MessageDetails
型別。除了 visible
每次都會更新,message
和 mode
不一定每次都需要,因此我們將這兩個屬性設為可選:
export type MessageDetails = {
visible: boolean
message?: string
mode?: 'error' | 'success'
}
然後更新 ActionType
:
type ActionType =
| { type: 'ADD_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: number }
| { type: 'EDIT_TODO_TITLE'; payload: { id: number; title: string } }
| { type: 'TOGGLE_TODO_ISFINISHED'; payload: number }
| { type: 'MESSAGE'; payload: MessageDetails }
我們需要為提示訊息定義狀態型別和初始狀態:
// 定義狀態
type StateType = {
todos: TodoItem[]
messageDetail: MessageDetails
}
// 初始狀態
const initialState: StateType = {
todos: [],
messageDetail: {
visible: false,
message: '',
mode: 'error',
},
}
然後在 reducer
中加入處理提示訊息的邏輯:
case 'MESSAGE':
return {
...state,
messageDetail: {
...state.messageDetail, // 保留現有的 message 屬性
...action.payload, // 只覆蓋傳入的屬性
},
}
刪除原來用 useState
定義的局部狀態:
const [messageDetails, setMessageDetails] = useState<MessageDetails>({
visible: false,
message: '',
mode: 'error',
})
更新前的 createTodoHandler
如下:
const createTodoHandler = (title: string) => {
if (title.trim().length === 0) {
setMessageDetails({
visible: true,
message: 'Input cannot be empty!',
mode: 'error',
})
return
}
dispatch({ type: 'ADD_TODO', payload: title })
setMessageDetails({
visible: true,
message: 'Todo created successfully!',
mode: 'success',
})
}
將 setMessageDetails
替換為 dispatch
,重構後:
const createTodoHandler = (title: string) => {
if (title.trim().length === 0) {
dispatch({
type: 'MESSAGE',
payload: {
visible: true,
message: 'Input cannot be empty!',
mode: 'error',
},
})
return
}
dispatch({ type: 'ADD_TODO', payload: title })
dispatch({
type: 'MESSAGE',
payload: {
visible: true,
message: 'Todo created successfully!',
mode: 'success',
},
})
}
我們不再需要將 props
傳給 Message
元件:
<Message />
移除所有 props
,並使用 useContext
來存取全域狀態:
import { useContext, useEffect } from 'react'
import { TodoContext } from '../store/TodoContext'
export default function Message() {
const { dispatch } = useContext(TodoContext)
const { visible, message, mode } = useContext(TodoContext).state.messageDetail
useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
dispatch({ type: 'MESSAGE', payload: { visible: false } })
}, 3000)
return () => clearTimeout(timer)
}
}, [visible])
return (
<div
className={`${mode === 'error' ? 'bg-red-500' : 'bg-green-500'} ${
visible ? 'flex' : 'hidden'
} rounded-[5px] p-[10px] fixed bottom-[20px] left-[20px]`}
>
<p className='text-[20px]'>{message}</p>
</div>
)
}
為編輯標題和勾選操作加入提示訊息:
// 提交新的標題
const submitNewTitle = () => {
// 如果標題是空的,則不更新
if (typeof newTitle === 'string' && newTitle.trim().length === 0) {
setNewTitle(children)
setIsEditing(false)
dispatch({
type: 'MESSAGE',
payload: {
visible: true,
message: 'Title cannot be empty!',
mode: 'error',
},
})
return
}
if (typeof newTitle === 'string' && newTitle.trim().length > 0) {
dispatch({
type: 'EDIT_TODO_TITLE',
payload: { id, title: newTitle },
})
dispatch({
type: 'MESSAGE',
payload: {
visible: true,
message: 'Title updated successfully!',
mode: 'success',
},
})
}
}
// 處理勾選
const checkboxHandler = () => {
dispatch({
type: 'TOGGLE_TODO_ISFINISHED',
payload: id,
})
dispatch({
type: 'MESSAGE',
payload: {
visible: true,
message: 'Todo updated successfully!',
mode: 'success',
},
})
}